/*
 * Copyright 2010 Alexander Ulizko
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.ulizko.template.soy.javaSrc;

import com.ulizko.template.soy.utils.StringUtils;
import com.google.common.base.Preconditions;
import japa.parser.JavaParser;
import japa.parser.ParseException;
import japa.parser.ast.Comment;
import japa.parser.ast.CompilationUnit;
import japa.parser.ast.body.BodyDeclaration;
import japa.parser.ast.body.MethodDeclaration;
import japa.parser.ast.body.TypeDeclaration;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.List;

/**
 * Analyzes generated java code and redecorates it in a more human-readable way.
 * For details, see {@link #analyze(String)}
 * @author Alexander <alexander@ulizko.com> Ulizko
 */
public class BlobJavaSrcAnalyzer {
    /**
     * Carefully preserve info about where from code was generated
     */
    private List<Comment> comments;
    /**
     * JavaParser's compilation unit
     */
    private CompilationUnit compilationUnit;
    /**
     * Collection of blank java classes
     */
    private HashMap<String, JavaClassBlank> blankClasses = new HashMap<String, JavaClassBlank>();

    /**
     * Analyzes java source code, created from {@link com.google.template.soy.SoyToJavaSrcCompilerExperimental}.
     * Basically, it iterates through generated methods, and make assumptions based on method name, to what class, package it belongs and what name it should have.
     * After that, it generates list of {@link JavaClassBlank} which can be retreived via {@link #getBlankClasses}.
     * <p/>
     * More on method name analyze: it takes it takes method name, generated by {@link com.google.template.soy.SoyToJavaSrcCompilerExperimental}
     * (like, for example, "com$sitename$template$pages$indexPage"),
     * splites it on "$". Last chunk it assumes as a method name, second-to-last as a class name,
     * and the rest as a java package.
     *
     * @param generatedCode Human-readable code.
     * @throws UnsupportedEncodingException Expected never to occur.
     * @throws ParseException
     */
    public void analyze(String generatedCode) throws UnsupportedEncodingException, ParseException {
        Preconditions.checkNotNull(generatedCode, "Provided generated code should not be null");
        Preconditions.checkArgument(generatedCode.trim().length() > 0, "Provided generated code is empty");
        
        InputStream in = new ByteArrayInputStream(wrapGeneratedCodeWithClassNotation(generatedCode).getBytes("UTF-8"));
        compilationUnit = JavaParser.parse(in);

        preserveComment();

        analyzeMethods();
    }

    /**
     * Takes all comments of the docs and stores it as class variable.
     */
    private void preserveComment() {
        comments = compilationUnit.getComments();
    }

    /**
     * Iterate through methods, collect them, and calculate, which humanized package, class and method they have.
     */
    private void analyzeMethods() {
        List<TypeDeclaration> types = compilationUnit.getTypes();

        // iterate through methods, collect them
        for (TypeDeclaration type : types) {
            List<BodyDeclaration> members = type.getMembers();
            for (BodyDeclaration member : members) {
                if (member instanceof MethodDeclaration) {
                    MethodDeclaration method = (MethodDeclaration) member;

                    // collect package name and class name from the method name
                    MethodDescription methodDescription = StringUtils.analyzeMethodName(method.getName());

                    // find adjacent comment and retreive from it from what file is current method generated
                    Comment adjacentComment = getAdjacentComment(method.getBeginLine());
                    String soyFileName = StringUtils.retreiveSoyFileNameFromComment(adjacentComment.getContent());

                    methodDescription.setSoyFileName(soyFileName);

                    // store method declaration
                    method.setName(methodDescription.getMethodName());
                    methodDescription.setMethod(method);

                    // build class identifier - based on packageName.className
                    String classIdentifier = methodDescription.getPackageName() + "." + methodDescription.getClassName();
                    JavaClassBlank javaClassBlank;
                    // try to find existed javaClassBlank by class identifier
                    javaClassBlank = blankClasses.get(classIdentifier);
                    if (javaClassBlank == null) { // i.e. there is no existed blank class
                        javaClassBlank = new JavaClassBlank();
                        javaClassBlank.setPackageName(methodDescription.getPackageName());
                        javaClassBlank.setClassName(methodDescription.getClassName());
                    }

                    javaClassBlank.addMethod(methodDescription);
                    // Shocked, but without that line the whole magic doesn't work
                    // Looks like HashMap stores deep copy of the object, not the link to the object.
                    // I was sure that java.util.List stores link to the object. Maybe, HashMap differs.   
                    blankClasses.put(classIdentifier, javaClassBlank);
                }
            }
        }
    }

    /**
     * Get's adjacent comment above provided method begine line number
     * @param methodBeginLineNumber Method begin line number
     * @return Adjacent comment
     */
    private Comment getAdjacentComment(int methodBeginLineNumber) {
        int commentBeginLine = 0;
        Comment resultComment = null;
        for (Comment comment : comments) {
            int currentCommentBeginLineNumber = comment.getBeginLine();
            if (currentCommentBeginLineNumber < methodBeginLineNumber && currentCommentBeginLineNumber > commentBeginLine) {
                resultComment = comment;
            }
        }

        return resultComment;
    }

    /**
     * Wraps generated java code with "class Generated {}" notation.
     * It demands JavaParser.
     *
     * @param generatedCode generated {@link com.ulizko.template.soy.javaSrc.SoyFileSetBuilder#compileToJavaSrc()} java code.
     * @return java code with class notation
     */
    private String wrapGeneratedCodeWithClassNotation(String generatedCode) {
        return "class Generated {\n" + generatedCode + "\n}";
    }

    /**
     * Returns list of generated {@link com.ulizko.template.soy.javaSrc.JavaClassBlank}.
     * <p/>
     * More on java class generation at {@link #analyze}.
     * @return list of generated java classes.
     */
    public HashMap<String, JavaClassBlank> getBlankClasses() {
        return blankClasses;
    }
}
